Skip to content

Disable pnpm's implicit install-on-run in favor of explicit installs - #5804

Open
lukemelia wants to merge 2 commits into
mainfrom
fix-ci-pnpm-implicit-installs
Open

Disable pnpm's implicit install-on-run in favor of explicit installs#5804
lukemelia wants to merge 2 commits into
mainfrom
fix-ci-pnpm-implicit-installs

Conversation

@lukemelia

@lukemelia lukemelia commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What

  1. Sets verifyDepsBeforeRun: false in pnpm-workspace.yaml. pnpm 11 defaults this setting to install, which makes every pnpm run / pnpm exec check node_modules freshness first and silently run a workspace-wide install when the check fails. With false, scripts run against node_modules as-is and installs happen only when explicitly requested — the pnpm 10 behavior this workspace ran on previously.
  2. Adds pnpm-workspace.yaml to every workflow path filter that already lists pnpm-lock.yaml (and to ci-host's index-cache invalidation case). The file carries dependency-resolution settings (catalog, overrides, patchedDependencies, allowBuilds, verifyDepsBeforeRun), but a settings-only change that doesn't touch the lockfile previously ran zero suites.

Why

The freshness check never passes in CI — not even immediately after the init action's pnpm install --frozen-lockfile, and not after one of the implicit installs itself completes — so every pnpm invocation in a CI job pays a redundant 4–16s install. Green host-shard logs show the sequence plainly: the explicit install finishes, then three more implicit installs run during setup, then several concurrent ones fire when run-p boots the test services.

That concurrency is the fatal case. start:matrix, start:smtp, start:host-dist, and the rest are each pnpm run invocations, so each spawns its own workspace-wide install into the same node_modules. They stomp each other's bin links (the Failed to create bin … ENOENT warnings visible even in passing shards) and occasionally wedge each other. When start:matrix's install is the one that hangs, start-matrix.sh never reaches pnpm assert-synapse-running, the Synapse container is never created, and the shard fails with Failed to reach Synapse at http://localhost:8008/_matrix/client/versions after 60 attempts (~300s) — the recurring "Synapse startup" flake. Failing-shard logs show that install still running at teardown, killed by SIGTERM five minutes after it started, with Error response from daemon: No such container: boxel-synapse-ci in between.

The same mechanism slows local dev: every pnpm <script> pays the check, and pnpm start:all fans out the same concurrent implicit installs.

Test plan

  • pnpm config get verify-deps-before-run reports false with this change (previously unset, defaulting to install).
  • With the path filters updated, CI on this PR exercises the fix directly: host-shard logs should show no Done in Ns using pnpm implicit-install output between the init action's install and the test run, and no Failed to create bin … ENOENT warnings during service boot.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files  ±    0      1 suites  ±0   2h 1m 39s ⏱️ + 1h 56m 6s
4 301 tests +4 055  4 287 ✅ +4 041  14 💤 +14  0 ❌ ±0 
4 320 runs  +4 074  4 306 ✅ +4 060  14 💤 +14  0 ❌ ±0 

Results for commit 3691ab4. ± Comparison against earlier commit d5e8aba.

Realm Server Test Results

    1 files  ±  0      1 suites  ±0   16m 8s ⏱️ + 2m 31s
2 174 tests +575  2 174 ✅ +575  0 💤 ±0  0 ❌ ±0 
2 254 runs  +618  2 254 ✅ +618  0 💤 ±0  0 ❌ ±0 

Results for commit 3691ab4. ± Comparison against earlier commit d5e8aba.

lukemelia and others added 2 commits August 18, 2026 16:58
…flake

pnpm 11 defaults verify-deps-before-run to `install`, so every `pnpm run` /
`pnpm exec` first checks node_modules freshness and silently runs a
workspace-wide install when the check fails. In CI the check never passes,
even immediately after the init action's `pnpm install --frozen-lockfile`
(and even after one of these implicit installs completes), so every pnpm
invocation in a job pays a redundant 4-16s install. The fatal case is the
service boot: `run-p` launches ~9 pnpm scripts at once, each kicking off its
own concurrent install into the same node_modules. They stomp each other's
bin links (the "Failed to create bin ... ENOENT" warnings visible in green
shards) and occasionally deadlock; when start:matrix's install is the one
that wedges, the Synapse container is never created and the shard fails
with "Failed to reach Synapse ... after 60 attempts (~300s)".

This is the mechanism behind the recurring "Synapse startup race" flake,
which hit 12 host shards across three PRs today alone. Setting
verifyDepsBeforeRun: false restores pnpm 10 behavior: scripts run against
node_modules as-is, and installs happen only when explicitly requested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pnpm 11 reads workspace settings (catalog, overrides, patchedDependencies,
allowBuilds, verifyDepsBeforeRun) from pnpm-workspace.yaml, but no workflow
path filter included it — a settings-only change that doesn't touch the
lockfile ran zero suites. Add it alongside pnpm-lock.yaml in every filter,
including ci-host's index-cache invalidation case, since overrides and
patches can change indexing behavior the same way a lockfile change can.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lukemelia
lukemelia force-pushed the fix-ci-pnpm-implicit-installs branch from d5e8aba to 3691ab4 Compare August 18, 2026 21:11
@lukemelia
lukemelia requested review from a team and backspace August 19, 2026 02:03

@richardhjtan richardhjtan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] I reviewed this as a configuration change with two independent halves: does the setting actually take effect (a mistyped key in pnpm-workspace.yaml fails silently), and is the path-filter change complete (a half-applied filter is worse than none, because it looks handled). I verified both against the checked-out branch rather than reading them.

Bottom line: both halves are correct and complete, and the reasoning in the description holds up. No blocking issues. One follow-up — the change fixes a symptom whose cause is still unexplained, and the artifact that would explain it is one cat away in CI.

The setting takes effect. pnpm config get verify-deps-before-run reports undefined on main and false on this branch, under the pinned pnpm 11.0.9. The key is top-level rather than nested under the catalog: map it follows — the mistake this file's shape invites.

The path-filter change is complete. I enumerated every reference to pnpm-lock.yaml under .github/ and checked each against the diff. Six sites carry it as a change-detection input — the paths: filters in ci-host.yaml, ci-software-factory.yaml, pr-boxel-ui.yml, preview-host.yml, the dorny/paths-filter block in ci.yaml, and the index-cache invalidation case in ci-host.yaml — and all six gained pnpm-workspace.yaml. None was missed.

The seventh reference is correctly left alone, which is worth recording because it looks like a miss. .github/actions/init/action.yml keys the actions/cache step on hashFiles('**/pnpm-lock.yaml'), and that cache's path is pnpm store path — the content-addressed package store, not node_modules. Settings like verifyDepsBeforeRun don't change which tarballs are needed, so adding the file there would only cost cache hits; and the resolution-affecting settings that do change them (catalog, overrides, patchedDependencies) cannot change without moving the lockfile, which already moves the key. Leaving it out is right in both directions.

Also worth noting the index-cache case is the right call, not just a consistent one. pnpm-workspace.yaml carries the catalog, so a version bump there changes what the indexer actually runs — which is the same reason packages/runtime-common/* is in that list. A settings-only edit will now invalidate the index cache unnecessarily, but erring that way costs a cache miss rather than a wrong green.

Follow-up: capture why the freshness check fails, before the comment in pnpm-workspace.yaml becomes the last word on it. Detail and the one-command diagnostic are in the thread on that block.


Generated by Claude Code

Comment thread pnpm-workspace.yaml
Comment on lines +260 to +267
# pnpm 11 defaults this to `install`, making every `pnpm run` first check
# node_modules freshness and silently run a full install when the check
# fails. The check never passes in CI, so each of the ~9 services `run-p`
# boots kicks off its own concurrent workspace-wide install into the same
# node_modules; when they collide badly enough, start:matrix never reaches
# `docker run` and the shard dies with "Failed to reach Synapse". Disabling
# restores pnpm 10 behavior: scripts run against node_modules as-is.
verifyDepsBeforeRun: false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Confirmation, plus the one question this change leaves open.

The setting takes effect — verified rather than assumed, since a mistyped or wrongly-placed key in pnpm-workspace.yaml fails silently:

pnpm --version                            → 11.0.9
main:        pnpm config get verify-deps-before-run → undefined
this branch: pnpm config get verify-deps-before-run → false

Placement is right too: the block is top-level, not nested under the catalog: map it follows, which is exactly the mistake this file's shape invites.

false is the correct value here, and it's worth saying why in the comment. pnpm also offers warn, error, and prompt, and warn is the obvious-looking compromise — keep the staleness signal, skip the install. It isn't one, given the premise of this change: if the check never passes in CI, warn emits a warning on every pnpm invocation in every job, and locally on every pnpm <script>. That's noise with no action attached. One sentence saying the middle values were considered and rejected for that reason would stop the next person from "improving" this to warn and then reverting it a week later.

The open question: why does the check fail? The description establishes that it does — including immediately after a clean pnpm install --frozen-lockfile — and that is enough to justify the change. But nothing here says why, and that matters for two reasons: if the divergence is real rather than a pnpm bug, this hides it; and if it is a pnpm bug, it is worth reporting upstream rather than carrying a workspace-local workaround indefinitely.

One command answers it. The check's inputs are recorded in node_modules/.pnpm-workspace-state-v1.json. On this workspace it holds:

lastValidatedTimestamp, projects (per-project manifests, keyed by absolute path),
pnpmfiles, settings, filteredInstall

Dumping that file in a CI job immediately after the init action's install, and again before the first pnpm run, shows which input moved. Two of the fields are already interesting here without any speculation about the cause:

  • pnpmfiles lists this repo's .pnpmfile.cjs. A pnpmfile is a resolution input, so it is one of the things the check compares.
  • filteredInstall is false, which matches CI — .github/actions/init/action.yml runs a single unfiltered pnpm install --frozen-lockfile, and it is the only pnpm install in .github/ — so a filtered/unfiltered mismatch is already ruled out.

Scope. Follow-up, not a blocker. The change stands on its measured effect; this is about not losing the thread on the cause, since the comment above will be read as the final word on it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants